//
//  JSBridge.js
//  JSBridge
//
//  Created by Siva RamaKrishna Ravuri
//  Copyright (c) 2014 www.siva4u.com. All rights reserved.
//
// The MIT License (MIT)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
//

;(function(w,doc) {
    if(w.JSBridge)return;

// PRIVATE VARIABLES
    //!!! WARNING - Should be in SYNC with Native Code defines - Begin
    var JSBRIDGE_URL_SCHEME  = 'jsbridgeurlscheme';
    var JSBRIDGE_URL_MESSAGE = '__JSB_URL_MESSAGE__';
    var JSBRIDGE_URL_EVENT   = '__JSB_URL_EVENT__';
    var JSBRIDGE_URL_API     = '__JSB_URL_API__';
  
    //!!! WARNING - Should be in SYNC with Native Code defines - End
  
    var ua                  = navigator.userAgent;
    var isIOSDevice         = /iP(hone|od|ad)/g.test(ua);
    var isAndroidDevice     = /Android/g.test(ua);
    var sendMessageQueue    = [];
    var receiveMessageQueue = [];
    var messageHandlers     = {};
    var responseCallbacks   = {};
    var apiData             = null;
    var uniqueId            = 1;
    var messagingIframe;

    var _fix_data           = /(\'_\#b_|_b\#_\'|\'_\#i_|_i\#_\'|\'_\#o_|_o\#_\')/g;

// PRIVATE METHODS

    function JSBridgeLog() {
        if (typeof console != 'undefined') {
            console.log("JSBridge:JS: LOG: ",arguments);
        }
    }
    function JSBridgeLogException(e,m) {
        if (typeof console != 'undefined') {
            console.error("JSBridge:JS: EXCEPTION: ",arguments);
        }
    }

    function getIFrameSrc(param) {
        return JSBRIDGE_URL_SCHEME + '://' + JSBRIDGE_URL_MESSAGE + '/'+ param;
    }

    function callObjCAPI(name,data) {
        // Should not called triggerNativeCall as iFrame needs to be deleted in order to get the retvalue.
        var iframe = document.createElement("IFRAME");
        apiData = {api:name};
        if(data) apiData["data"] = data;
        iframe.setAttribute("src", getIFrameSrc(JSBRIDGE_URL_API));
        document.documentElement.appendChild(iframe);
        iframe.parentNode.removeChild(iframe);
        iframe = null;

        var ret = JSBridge.nativeReturnValue;
        JSBridge.nativeReturnValue = undefined;
        if(ret) return decodeURIComponent(ret);
    };

    function triggerNativeCall() {
        if(isIOSDevice) {
            messagingIframe.src = getIFrameSrc(JSBRIDGE_URL_EVENT);
        } else {
            var apiName = ((isAndroidDevice)?("AndroidAPI.ProcessJSEventQueue"):("WebAppAPI.ProcessJSEventQueue"));
            try {
                var api = eval(apiName);
                if(api) api(_fetchJSEventQueue());
            } catch(e) {}
        }
    }

    function doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message['callbackId'] = callbackId;
        }
        sendMessageQueue.push(message);
        triggerNativeCall();
    }

    function dispatchMessageFromNative(messageJSON) {
        setTimeout(function _timeoutDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;

            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if(!responseCallback){return;}
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        doSend({responseId:callbackResponseId, responseData:responseData});
                    }
                }
                
                try {
                    var handler = ((message.eventName)?(messageHandlers[message.eventName]):(JSBridge.bridgeHandler));
                    if(handler) {
                    	handler(message.data, responseCallback);
                    }
                } catch(e) {
                    JSBridgeLogException(e,"dispatchMessageFromNative");
                }
            }
        });
    }

    function getReturnObject(apiName, status, dataJson) {
        var outJson = {status : status};
        if(apiName) outJson["apiName"] = apiName;
        if(dataJson) outJson["data"] = dataJson;
        return outJson;
    }


// PUBLIC METHODS
    function init(bridgeHandler) {
        if(JSBridge.bridgeHandler){JSBridgeLogException(e,"init");}
        JSBridge.bridgeHandler  = bridgeHandler;
        var receivedMessages    = receiveMessageQueue;
        receiveMessageQueue     = null;
        for(var i=0; i<receivedMessages.length; i++) {
            dispatchMessageFromNative(receivedMessages[i]);
        }
    }

    function send(eventName, data, responseCallback) {
        var dataToSend = {};
        if(eventName) dataToSend["eventName"] = eventName;
        dataToSend["data"] = {status : "true"};
        if(data) dataToSend["data"]["data"] = data;
        doSend(dataToSend, responseCallback);
    }

    function registerEvent(eventName, handler) {
        messageHandlers[eventName] = handler;
    }

    function deRegisterEvent(eventName, handler) {
        if(messageHandlers[eventName]) {
            delete messageHandlers[eventName];
        }
    }

    function callAPI(name, data, responseCallback) {
        data = data !== undefined ? data : {};

        try {
            if(data) {
                for(var key in data) {
                    var obj = data[key],
                        opt = typeof obj;

                    switch(opt) {
                        case "number":
                            data[key] = '_#i_'+obj+'_i#_';
                            break;
                        case "boolean":
                            data[key] = '_#b_'+obj+'_b#_';
                            break;
                        case "undefined":
                            data[key] = '_#o_'+obj+'_o#_';
                            break;
                        default: 
                            if (obj == null) {
                                data[key] = '_#o_'+obj+'_o#_';
                            }
                    }
                }

                if(responseCallback) {
                    var cbID = "cbID" + (+new Date);
                    responseCallbacks[cbID] = responseCallback;
                    data["callbackID"] = cbID;
                }
                try{data = JSON.stringify(data);}catch(e){}
            }

            if(isIOSDevice) {
                var ret;

                if(data) name += ":";
                ret = callObjCAPI(name,data);

                ret = ret.replace(_fix_data, '');
                ret = ret.replace(/\'/g, '"');
                ret = ret.replace(/\"\{/g, '{');
                ret = ret.replace(/\}\"/g, '}');
                ret = ret.replace(/\,\"data\"\:undefined/, '');

                return JSON.parse(ret).data;
            } else {
                var api = eval((isAndroidDevice)?("AndroidAPI.ProcessJSAPIRequest"):("WebAppAPI.ProcessJSAPIRequest"));
                if(api) {
                	if(data) return api(name,data);
                	return api(name,null);
                } else {
                    JSBridgeLogException("Unsupported API:",name);
                }
            }
        } catch(e) {
            JSBridgeLogException(e,"Invalid API:"+name);
        }
    }

    function callAPICallback(apiCallback, outJson, status) {
        if(apiCallback) {
            apiCallback(getReturnObject(null,((status)?(status):("true")),outJson));
        }
    }
    function callEventCallback(responseCallback, outJson, inJson) {
        if(responseCallback) {
            responseCallback(getReturnObject(((inJson)?(inJson["eventName"]):(null)),"true", outJson));
        }
    }
  
    function _fetchJSEventQueue() {
        try {
            var messageQueueString = JSON.stringify(sendMessageQueue);
            sendMessageQueue = [];
            return messageQueueString;
        } catch(e) {
            JSBridgeLogException(e,"_fetchJSEventQueue");
        }
        return [];
    }

    function _handleMessageFromNative(messageJSON) {
        if(receiveMessageQueue) {
            receiveMessageQueue.push(messageJSON);
        } else {
            dispatchMessageFromNative(messageJSON);
        }
    }

    function _getAPIData() { return JSON.stringify(apiData); }
  
    function _invokeJSCallback(cbID,removeAfterExecute,config) {
        if(cbID) {
            var cb = responseCallbacks[cbID];
            if(cb) {
                if(removeAfterExecute) delete(responseCallbacks[cbID]);
                var data = config;
                if(isAndroidDevice) {
                    try {data = JSON.parse(config);}catch(e){}
                }
                if(data.callbackID) delete(data.callbackID);
                cb.call(null, data);
            }
        }
    };

    w.JSBridge = {
        init    : init.bind(this),
        send    : send.bind(this),
        callAPI : callAPI.bind(this),
  
        registerEvent   : registerEvent.bind(this),
        deRegisterEvent : deRegisterEvent.bind(this),

        callAPICallback     : callAPICallback.bind(this),
        callEventCallback   : callEventCallback.bind(this),
  
        _fetchJSEventQueue      : _fetchJSEventQueue.bind(this),
        _handleMessageFromNative: _handleMessageFromNative.bind(this),
        _getAPIData             : _getAPIData.bind(this),
        _invokeJSCallback       : _invokeJSCallback.bind(this),
    }

    messagingIframe = doc.createElement('iframe');
    messagingIframe.style.display = 'none';
    triggerNativeCall();
    doc.documentElement.appendChild(messagingIframe);

    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('JSBridgeReady');
    readyEvent.bridge = JSBridge;
    doc.dispatchEvent(readyEvent);


    window.$J = {
        call : JSBridge.callAPI,
    };
})(window,document);
